docs(openspec): archive add-vale-binary-packages and land the cli pin - #91
Conversation
There was a problem hiding this comment.
Pull request overview
Archives the add-vale-binary-packages OpenSpec change by moving its artifacts into openspec/changes/archive/2026-08-07-add-vale-binary-packages/ and promoting the change’s spec deltas into openspec/specs/ (including a new vale-binary-packages capability).
Changes:
- Added the new
vale-binary-packagescapability spec describing per-platform Vale binary npm packages and their release/verification constraints. - Promoted new/updated requirements into existing
cliandinfrastructurespecs to reflect Vale platform-package pinning (viaoptionalDependencies) and changesets exclusion for script-versioned packages. - Added archived change artifacts (proposal, design, tasks, and delta specs) under the dated archive directory.
Reviewed changes
Copilot reviewed 3 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| openspec/specs/vale-binary-packages/spec.md | New capability spec defining requirements for Vale per-platform binary packages. |
| openspec/specs/infrastructure/spec.md | Adds a requirement about excluding workflow-versioned packages from changesets. |
| openspec/specs/cli/spec.md | Adds a requirement describing the CLI’s Vale platform packages as exact-version optionalDependencies. |
| openspec/changes/archive/2026-08-07-add-vale-binary-packages/tasks.md | Archived task checklist for the change (including remaining unchecked items). |
| openspec/changes/archive/2026-08-07-add-vale-binary-packages/specs/vale-binary-packages/spec.md | Delta spec capturing added requirements for the new capability. |
| openspec/changes/archive/2026-08-07-add-vale-binary-packages/specs/infrastructure/spec.md | Delta spec capturing added infrastructure requirements (changesets ignore). |
| openspec/changes/archive/2026-08-07-add-vale-binary-packages/specs/cli/spec.md | Delta spec capturing added CLI requirements (Vale optionalDependencies pin). |
| openspec/changes/archive/2026-08-07-add-vale-binary-packages/proposal.md | Archived proposal describing scope, impact, and delivery shape. |
| openspec/changes/archive/2026-08-07-add-vale-binary-packages/design.md | Archived design rationale and decisions for packaging/versioning/release approach. |
| openspec/changes/archive/2026-08-07-add-vale-binary-packages/.openspec.yaml | Archive metadata for the change directory. |
4120280 to
631fd3f
Compare
Addressed. The one inline finding — the This branch was also restacked onto the rewritten — AI Coding Agent |
631fd3f to
867c5f7
Compare
867c5f7 to
157cd06
Compare
|
Blocked by #92. #92 gates the vale publish path on the pinned Vale version rather than on the manifest file being touched, and amends Still also waiting on tasks 6.1–6.3 (the CLI — AI Coding Agent |
…st file The publish path fires on a push to main touching vale-manifest.json, and a `paths:` filter cannot see WHY the file changed. A reworded comment, a reformat, or a digest correction is indistinguishable from a version bump, and each one published six packages at a fresh <valeVersion>-<timestamp>. Nothing downstream absorbs that: every stamp is novel by construction, so there was no second line of defense. Add a credential-free `gate` job ahead of `prepare` that asks whether the pinned Vale version is already published, and skips when it is. It runs before prepare downloads ~60 MB, so a skip is cheap. design.md D5 argued an already-published check cannot work here. That is true of the STAMPED version and false of the BASE version, and the difference is the whole design: "is 3.17.1-20260810000724 published?" is always no, while "has anything been published for Vale 3.17.1?" is answered by a published 3.17.1 or any 3.17.1-* stamp. D5 is amended to draw that line, so the gate does not read as contradicting it. Two properties kept deliberately: - An explicit workflow_dispatch passes --force and is never suppressed. A human asking for a publish gets one. - A skip requires ALL six packages to carry the pinned version. Checking one would silently skip a half-published set, so the gate doubles as partial-release repair. Blocks #91: that PR archives this change, so its design.md must carry the amended D5 before it lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
157cd06 to
1d48afa
Compare
Completes tasks 6.1-6.3 of add-vale-binary-packages, which were blocked until
the packages existed on npm. All six are now published at
3.17.1-20260810052605, so the CLI pins that exact version.
The pin is literal, per D8, and that needed checking rather than assuming:
`packages/*` makes the six vale packages workspace members at 0.0.0, so pnpm
could have linked them locally and silently reproduced the `workspace:*`
behaviour D8 rejects. The lockfile records a registry resolution and
node_modules points into the store at the pinned version, so the literal pin
means what it says.
6.2 verified end to end: createRequire from the CLI resolves the package, the
sibling `vale` is mode 755 and reports `vale version 3.17.1`, and only the
host-matching package installs — the other five are filtered by os/cpu.
6.3 verified against two real published stamps rather than by argument: with
dist-tags.latest at 3.17.1-20260810052605, a project pinned to
3.17.1-20260810045147 installs 045147. `^3.17.1` matches no version at all, so
a range cannot float into a prerelease.
Adds the changeset the proposal calls for ("Release impact: minor"). This is
why `skip-changeset` no longer applies to this PR: it was accurate while the
change was docs-only, and the pin makes it a real release — installing the CLI
now also delivers a binary.
7.3 stays open here by necessity; it edits add-vale-rule-engine's tasks, which
exist only on #71.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
efecade to
1133a3d
Compare
Stack (root → tip):
Unit 3 of
add-vale-binary-packages, stacked on #86.What is here today
Only the archive:
openspec/changes/add-vale-binary-packages/moves toopenspec/changes/archive/2026-08-07-add-vale-binary-packages/, and its deltas are promoted intoopenspec/specs/(cli,infrastructure, and a newvale-binary-packagescapability).Splitting the archive out of #86 is what makes the stack legible. #86 was the tip while still carrying an unarchived change directory, so
stack: openspec-archivedfailed there. With this branch on top, #86 is no longer the tip and its gate skips, while this PR carries the archive and satisfies it.What is still missing
Tasks 6.1, 6.2, 6.3 and 7.3 are unchecked, and they stay unchecked until the work lands here:
packages/cligains the six@taskless/vale-*packages inoptionalDependencies, pinned at literal exact stamped versions. Notworkspace:*, which would silently re-point at the newest stamp, and notdevDependencies, which consumers never install.That pin cannot be written yet. An exact version can only name something that exists on npm, and none of the six names are published. Publishing them the first time needs a manual bootstrap, because a trusted publisher cannot be registered for a package that does not exist.
Sequence to green
@taskless/vale-*names on npm by hand.phase: publishto exercise OIDC end to end and produce a real stamped version set.optionalDependenciespin on this branch against those stamped versions, check off tasks 6.x and 7.3, and mark this PR ready.Why this is a draft
Archiving the change before the CLI pin exists would record the work as finished while the piece that makes it useful to consumers is still unwritten. The draft state is the guard: it keeps this branch unmergeable until step 6 above is done. A draft still counts for stack-tip detection, so #86's gate flips regardless.
Fixes OSS-22
Built on top of #72
Publishes the Vale binary as per-platform npm packages from this repo, so a first-class engine isn't a host prerequisite.
This PR now carries unit 1 of a forward-merging stack: the six
packages/vale-<platform>/workspace packages, the committed checksum manifest that pins what goes into them, and the changesetsignoreentries that keeprelease.ymlout of their versions. It publishes nothing and no consumer references it. Unit 2 (#86) adds the fetch, verify, stamp, and two-phase release workflow. Unit 3, the CLI'soptionalDependenciespin, cannot be written until these names exist on npm.Why binary-in-tarball
The only existing npm distribution,
@vvago/vale, is third-party and downloads at postinstall. That script runs during a consumer's install under a policy we don't set — pnpm 10 blocks dependency build scripts by default — producing no binary and no error. The objection is mechanism, not provenance: it would stand if the Vale project published it. Binary-in-tarball is integrity-hashed, lockfile-pinned, resolves offline, and needs no lifecycle script. Sopackages/vale-<platform>/carries the binary withos/cpudeclared and nobin, no code, no scripts — ast-grep's packaging without ast-grep's installation, whose hardlink step already fails here underpnpm dlx.Versioning
An all-prerelease timestamp,
<valeVersion>-<yyyymmddhhmmss>, with a plain<valeVersion>never published. That keeps the Vale version legible, means a packaging fix is a new timestamp on the same base rather than a spent version, and — because a prerelease only satisfies a range naming the samemajor.minor.patch— makes^3.17.1provably unable to resolve. Exact pinning stops being a convention someone can drift from.Binaries are not committed
Six platforms at 10–20 MB each would live in git history permanently, so a published tarball is not reproducible from a plain clone. SHA256 checksums are committed and reviewed, and the pipeline refuses a mismatch, keeping "what can merge to main" as the trust boundary. The workflow that consumes them (#86) runs in two phases — detect upstream on a schedule and open a PR with the new version and checksums, then publish on merge — so nobody has to notice a Vale release and nothing publishes on bytes nobody signed off on. Safe to automate because publishing is inert: the CLI pins a literal exact version, so a new package reaches nobody until that pin is deliberately bumped.
Resolved open questions
The proposal left three open. All three are answered, and the reasoning is written up in
design.mdunder Resolved Questions.The matrix is six packages, not ast-grep's seven. Vale 3.17.1 publishes exactly six binary assets, and the packages are those six:
darwin-arm64,darwin-x64,linux-arm64,linux-x64,win32-arm64,win32-x64. ast-grep's seventh iswin32-ia32, and Vale ships no 32-bit Windows asset, so there is nothing to package.No libc or toolchain suffix in the names —
vale-linux-x64, notvale-linux-x64-gnu;vale-win32-x64, not-msvc. ast-grep carries those suffixes because Rust target triples disambiguate several builds per platform. Vale publishes exactly one build peros/cpupair, so a suffix would disambiguate nothing while asserting a toolchain nobody verified.musl stays on the
PATHfallback. Upstream publishes no musl asset, so there is nothing to package for Alpine. That is not only a packaging gap: Vale's Linux build is dynamically linked against glibc (verified asdynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0), so it is not a static Go binary and would not run on musl even if it were installed there. The linux packages' READMEs say so plainly rather than leaving a user to discover it as a loader error. This matches the existing gap rather than widening it:findSgBinary()maps every Linux to-gnutoday, so Alpine already falls through for ast-grep.Vale 3.17.1 is the pinned version, recorded in
.github/scripts/vale-manifest.jsonbeside the scripts that consume it. The manifest holds the version once, and per platform the asset-name template, the archive member to unpack, and the SHA256 of the release archive — upstream'svale_<version>_checksums.txtcovers the archives rather than the executables inside them, so a committed digest is independently checkable against upstream and the archive is verified before anything is unpacked from it. Tracking is the detect phase in #86: a weekly schedule opens a PR whenever upstream is ahead, and a security release takes a manualdetectdispatch rather than waiting for the cadence.Which Vale version the CLI pins is a separate decision, made when the CLI's
optionalDependenciesland in unit 3.Also carries two
CLAUDE.mdfixesUnrelated to Vale but too small to spend PRs on:
TSKL-, reading as though it's the only bare identifier the Linear integration resolves. It isn't —OSS-23linked and moved to In Review on PR creation for ref(cli): resolve ast-grep without an install-time step #69.git clone --depth=Nimplies--single-branch, which breaks--force-with-leaseon every branch (it failsstale info, so people fall back to a bare--force) and, more quietly, makesgit rebase maincorrect only while the merge base sits inside the shallow window.Where this sits
This change is the one exception to "one change, one PR": it is stacked, merging forward, with the archive landing on the last unit. The archive gate skips a PR that is not the tip, so
stack: openspec-archivedis not expected on this PR at all — #86 is the tip, and the change is archived on unit 3.ignoreoptionalDependenciespinpartition-rules-by-enginehas landed and is archived, so #71's only remaining prerequisite is a published binary from this stack.#70 and #71 are coupled by exactly one line: whichever lands second adds the engine-selection topic to
TOPICS. Ordering between them doesn't matter.Downstream, the generator's decision router (TSKL-279) needs a published release containing #70 and #71. It consumes a normal release — no prerelease, no path dependency — so it waits without blocking anything here.
skip-changesetstays on this PR. The six packages are in the changesetsignorelist andpackages/cliis untouched, so there is nothing here for changesets to version or release. Unit 3 is where a changeset belongs, since that is where a published CLI actually changes.Fixes OSS-22
Built on top of #86
Unit 2 of the
add-vale-binary-packagesstack. Unit 1 (#72) added six empty packages and the pinned manifest; this adds the pipeline that fills and publishes them, along with the scripts it runs and their tests. Unit 3, the CLI'soptionalDependenciespin, is not here and cannot be written until these names exist on npm.Two phases, because the trust boundary is code review
detectruns on a weekly schedule with no npm credential and no OIDC identity. It compares the latest upstream Vale release against the version pinned in.github/scripts/vale-manifest.json, and when upstream is ahead it opens a pull request updating that version and all six SHA256 digests, taken from upstream's ownvale_<version>_checksums.txt. It publishes nothing.publishruns on the push tomainthat merges that pull request, once a human has read the digests.The split is what makes the automation trustworthy. A single job that discovered a digest and then verified its downloads against the digest it had just discovered would verify nothing at all: whatever it downloaded would match, because the digest came from the same fetch. Separating discovery from verification puts a review in between, so nothing publishes on bytes nobody signed off on, and nobody has to notice a Vale release for the process to run.
What bounds a run
The upstream-version comparison, and only that. The "is this version already on npm?" check
release.ymluses cannot work here. Every publish stamps<valeVersion>-<yyyymmddhhmmss>, a version npm has never seen, so such a check would answer "not published" on every single run and could never suppress anything. The comparison against upstream is the only thing that can say "nothing to do."Why
prepareandpublishare separate jobspreparedownloads third-party bytes off the internet. It holdscontents: read, no environment, and noid-token, so it cannot publish or mint a token regardless of what it downloads. It verifies every archive against the committed digest and aborts the run on a mismatch before anything is unpacked, then hands overnpm packtarballs.The credentialed
publishjob therefore only ever handles bytes that already matched a reviewed digest and are already sealed into a tarball. It does not even check out the repository.Why packing comes before the artifact upload
actions/upload-artifactdoes not preserve file modes, and the Vale executable has to reach npm with its executable bit set.npm packrecords modes inside the.tgz, so packing first and shipping the tarball through the artifact keeps0755intact end to end.Before this can merge
package.jsoncarries the placeholder version0.0.0and no binary, so a barenpm publishfrom a package directory would burn the name on an empty0.0.0.npm-productionenvironment. There is no fallback token path in this workflow on purpose.phase: publishto exercise the OIDC path end to end.Merging neither PR in this stack publishes anything
The publish phase triggers on a push to
mainthat touches.github/scripts/vale-manifest.json. Unit 1 adds that manifest but no workflow to fire on it, and this PR adds the workflow but does not touch the manifest. The path filter never matches on either merge. The first publish is always deliberate, whether that is the manual bootstrap or aworkflow_dispatch.Known inherited limitation: the detect PR needs a manual check re-run
The detect phase opens its pull request with
GITHUB_TOKEN, and GitHub does not fire workflows on events raised by that token. SoValidatewill not start on a detect PR, and a maintainer has to re-run checks by hand before merging.This is the same step the changesets "Version Packages" PR already needs. Verified:
Validateon #65 ran withrun_attempt: 2, re-run manually before it merged. It is a limitation inherited from how GitHub scopesGITHUB_TOKEN, not a defect in this workflow.Stack
Forward-merging, per the proposal's delivery table. Unit 1 is repository-only and publishes nothing. Unit 2 publishes packages no consumer references yet. Unit 3 pins packages that by then exist.
This PR is a draft because it is the tip of the stack until unit 3 exists, and the OpenSpec archive gate would otherwise ask it to archive a change that is not finished. It also must not merge before the npm bootstrap above.
skip-changesetis correct here for the same reason it is on #72: the six packages are in the changesetsignorelist, andpackages/cliis untouched. Unit 3 is where a changeset belongs, since that is where a published CLI actually changes.Refs OSS-22